package com.jintian.smart.kernel.common.switching;

import java.io.IOException;
import java.nio.file.*;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.*;

public class FileChangeMonitor {

	private static final WatchService watcher = newWatchService();
	private static final Thread listenerThread;
	private static final ExecutorService dispatcherExec = Executors.newCachedThreadPool();
	private static final ConcurrentMap<Path, Directory> register = new ConcurrentHashMap<>();

	static {
		if (watcher == null) {
			throw new IllegalStateException("FileChangeMonitor could not be created!");
		} else {
			Runtime.getRuntime().addShutdownHook(new Thread(new ShutDownTask()));
			listenerThread = new Thread(new WatcherTask());
			listenerThread.setName("FileChangeEventWatch");
			listenerThread.setDaemon(true);
			listenerThread.start();

		}
	}

	private static WatchService newWatchService() {
		try {
			return FileSystems.getDefault().newWatchService();
		} catch (IOException ex) {
			return null;
		}
	}

	public static void register(FileChangeListener listener, Path file) {
		try {
			if (Files.isDirectory(file)) {
				final Path dirPath = file.toRealPath();
				Directory dir = register.get(dirPath);
				if (dir == null) {
					synchronized (register) {
						dir = register.get(dirPath);
						if (dir == null) {
							WatchKey key = dirPath.register(watcher, StandardWatchEventKinds.ENTRY_CREATE,
									StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
							dir = new Directory(key);
							register.put(dirPath, dir);
						}
					}
				}
				dir.addDirectoryListener(listener);
			} else if (Files.isRegularFile(file)) {

			}
		} catch (IOException ex) {

		}
	}

	public static void unregister(FileChangeListener listener) {
		Iterator<Entry<Path, Directory>> itor = register.entrySet().iterator();
		while (itor.hasNext()) {
			Entry<Path, Directory> e = itor.next();
			final Directory directory = e.getValue();
			directory.directoryListeners.remove(listener);
			if (directory.directoryListeners.isEmpty()) {
				itor.remove();
				directory.key.cancel();
				System.out.println("unwatch " + e.getKey());
			}
		}
	}

	public static void unregister(FileChangeListener listener, Path file) {
		try {
			if (Files.isDirectory(file)) {
				final Path dirPath = file.toRealPath();
				final Directory dir = register.get(dirPath);
				if (dir != null) {
					dir.directoryListeners.remove(listener);
					if (dir.directoryListeners.isEmpty() /* && dir.registeredFiles.isEmpty() */) {
						register.remove(dirPath);
						dir.key.cancel();
						System.out.println("unwatch " + dirPath);
					}
				}

			} else if (Files.isRegularFile(file)) {

			}
		} catch (IOException ex) {
		}
	}

	private static class Directory {
		private final CopyOnWriteArrayList<FileChangeListener> directoryListeners;
		private final WatchKey key;

		private volatile long lastEventTime;

		private Directory(WatchKey key) {
			this.key = key;
			directoryListeners = new CopyOnWriteArrayList<>();
		}

		private void addDirectoryListener(FileChangeListener listener) {
			directoryListeners.addIfAbsent(listener);
		}

		private void eventCreate(Path directoryPath, Path fileName) throws IOException {
			final Path file = directoryPath.resolve(fileName);
			for (FileChangeListener l : directoryListeners) {
				l.fileCreated(file);
			}
		}

		private void eventModify(Path directoryPath, Path fileName) throws IOException {
			final Path file = directoryPath.resolve(fileName);
			for (FileChangeListener l : directoryListeners) {
				l.fileModified(file);
			}
		}

		private void eventDelete(Path directoryPath, Path fileName) throws IOException {
			final Path file = directoryPath.resolve(fileName);
			for (FileChangeListener l : directoryListeners) {
				l.fileDeleted(file);
			}
		}
	}

	private static class ShutDownTask implements Runnable {
		@Override
		public void run() {
			try {
				watcher.close();
			} catch (IOException ex) {

			}
		}
	}

	private static class WatcherTask implements Runnable {

		@Override
		public void run() {
			while (true) {
				final WatchKey key;
				try {
					key = watcher.take();
				} catch (Exception x) {
					x.printStackTrace();
					return;
				}
				final List<WatchEvent<?>> events = key.pollEvents();
				try {
					Path eventPath = ((Path) key.watchable()).toRealPath();
					final Directory directory = register.get(eventPath);
					if (directory != null) {
						directory.lastEventTime = System.currentTimeMillis();
						dispatcherExec.submit(new DispatcherTask(directory, events));
					}
					for (WatchEvent<?> event : events) {
						if (event.kind().equals(StandardWatchEventKinds.ENTRY_DELETE)) {
							Set<Path> keys = new HashSet<>(register.keySet());
							final Path realPath = eventPath.resolve((Path) event.context());
							for (Path p : keys) {
								if (p.startsWith(realPath)) {
									Directory dir = register.remove(p);
									if (dir != null) {
										dir.key.cancel();
										System.out.println("unwatch " + p);
									}
								}
							}
						}
					}
				} catch (IOException iox) {
					// path is gone!
				}
				key.reset();
			}
		}
	}

	private static class DispatcherTask implements Runnable {

		private final Directory directory;
		private final List<WatchEvent<?>> events;

		private DispatcherTask(Directory directory, List<WatchEvent<?>> events) {
			this.directory = directory;
			this.events = events;
		}

		@SuppressWarnings("unchecked")
		@Override
		public void run() {
			try {
				final Path directoryPath = ((Path) directory.key.watchable()).toRealPath();
				for (WatchEvent<?> e : events) {
					try {
						final WatchEvent.Kind<?> kind = e.kind();
						if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
							directory.eventCreate(directoryPath, ((WatchEvent<Path>) e).context());
						} else if (kind == StandardWatchEventKinds.ENTRY_MODIFY) {
							directory.eventModify(directoryPath, ((WatchEvent<Path>) e).context());
						} else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
							directory.eventDelete(directoryPath, ((WatchEvent<Path>) e).context());
						}
					} catch (IOException ex) {
					}
				}
			} catch (IOException ex) {
			}
		}
	}

}
